My experiments with Haskell and GTK4 were heading towards something concrete, namely a GUI for a solver of Hashi puzzles. I had previously found Harald Bögeholz’s 2013 [open-source solver](https://github.com/ctbo/hashi) and forked it to create a command line application.
hashi-solver.exe
With a GUI application, it is perhaps easiest to start the journey with the destination:
The application starts with an empty problem of default minimum size 6 by 4. The width and the height of the problem can be resized using spinner buttons and islands can be placed in, or removed from, the sea by left-clicking with a mouse. If there is no solution or no unique solution, that is reported. If there is a unique solution the button becomes sensitive and indicates the option to solve the puzzle. A problem cannot be reduced in size if an island is in the way; the island must be removed first. A solved puzzle cannot be resized. It can, however, be reset to an empty problem but with the existing width and height.
The two spin buttons and their labels are packed in a horizontal box, so that their relative position does not change when the problem is resized. The window was specified as #resizable := False
, so that it both grew and shrank with its content. If it was resizable, it would grow but not shrink. The picture was specified as #canShrink := False
and #contentFit := Gtk.ContentFitCover
so that it remained at the intended size.
Mutable application state
I extended the mutable application state to be the problem, the list of solutions and a Bool
value to indicate if the application was in the solved state:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
data AppState = AppState { appProblem :: Problem , appSolutions :: [State] , appSolved :: Bool } main :: IO () main = do let problemInit = emptyProblem widthGridDefault heightGridDefault appStateRef <- newIORef $ AppState problemInit [] False app <- new Gtk.Application [ #applicationId := "com.pilgrem.hashi" , On #activate (activate ?self appStateRef) ] void $ app.run Nothing |
Sequencing
The principle challenge was sequencing in the action yielded by activate
, as actions that handle signals may depend on values yielded by other actions. The basic sequence that I applied was:
- Get the initial application state
- Create the values for the elements: the
Grid
, theBox
, the twoSpinButton
values and theirLabel
values, thePicture
and itsGestureClick
, and theButton
- Specify the actions that would handle signals: the button being clicked, the mouse being clicked, the value of a spin button changing
- Associate the actions that handle signals with the values for the elements: the button being clicked with the button, the value of a spin button changing with the spin buttons, the mouse being clicked with the gesture click being pressed
- Setup the button and the picture based on the initial application state
- Attach values to the grid, including the box, append values to the box, and add the gesture click to the picture
- Create the
HeaderBar
for the window and setup the header bar - Create the value for the
Window
- Show the window